package rexsee.barcode;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.regex.Pattern;

import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.Camera;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.Build;
import android.os.IBinder;
import android.os.Vibrator;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toast;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import com.google.zxing.ResultMetadataType;
import com.google.zxing.ResultPoint;
import com.google.zxing.client.result.ParsedResult;
import com.google.zxing.client.result.ResultParser;

public final class BarcodeCaptureDialog extends Dialog {

	private static final long BULK_MODE_SCAN_DELAY_MS = 1000L;
	private static final float BEEP_VOLUME = 0.10f;
	private static final long VIBRATE_DURATION = 200L;
	private static final int TEN_DESIRED_ZOOM = 27;
	private static final Pattern COMMA_PATTERN = Pattern.compile(",");

	private static final int MIN_FRAME_WIDTH = 240;
	private static final int MIN_FRAME_HEIGHT = 240;
	private static final int MAX_FRAME_WIDTH = 480;
	private static final int MAX_FRAME_HEIGHT = 360;

	private static final Set<ResultMetadataType> DISPLAYABLE_METADATA_TYPES;
	static {
		DISPLAYABLE_METADATA_TYPES = new HashSet<ResultMetadataType>(5);
		DISPLAYABLE_METADATA_TYPES.add(ResultMetadataType.ISSUE_NUMBER);
		DISPLAYABLE_METADATA_TYPES.add(ResultMetadataType.SUGGESTED_PRICE);
		DISPLAYABLE_METADATA_TYPES.add(ResultMetadataType.ERROR_CORRECTION_LEVEL);
		DISPLAYABLE_METADATA_TYPES.add(ResultMetadataType.POSSIBLE_COUNTRY);
	}

	private static final Vector<BarcodeFormat> FORMATS_PRODUCT;
	private static final Vector<BarcodeFormat> FORMATS_ONE_D;
	private static final Vector<BarcodeFormat> FORMATS_QR_CODE;
	private static final Vector<BarcodeFormat> FORMATS_DATA_MATRIX;
	static {
		FORMATS_PRODUCT = new Vector<BarcodeFormat>(5);
		FORMATS_PRODUCT.add(BarcodeFormat.UPC_A);
		FORMATS_PRODUCT.add(BarcodeFormat.UPC_E);
		FORMATS_PRODUCT.add(BarcodeFormat.EAN_13);
		FORMATS_PRODUCT.add(BarcodeFormat.EAN_8);
		FORMATS_PRODUCT.add(BarcodeFormat.RSS14);
		FORMATS_ONE_D = new Vector<BarcodeFormat>(FORMATS_PRODUCT.size() + 4);
		FORMATS_ONE_D.addAll(FORMATS_PRODUCT);
		FORMATS_ONE_D.add(BarcodeFormat.CODE_39);
		FORMATS_ONE_D.add(BarcodeFormat.CODE_93);
		FORMATS_ONE_D.add(BarcodeFormat.CODE_128);
		FORMATS_ONE_D.add(BarcodeFormat.ITF);
		FORMATS_QR_CODE = new Vector<BarcodeFormat>(1);
		FORMATS_QR_CODE.add(BarcodeFormat.QR_CODE);
		FORMATS_DATA_MATRIX = new Vector<BarcodeFormat>(1);
		FORMATS_DATA_MATRIX.add(BarcodeFormat.DATA_MATRIX);
	}

	CaptureHandler mCaptureHandler;
	final ViewfinderView mViewfinderView;
	private MediaPlayer mediaPlayer;

	private final Context mContext;
	private final BarcodeCaptureDialog mDialog;
	private final OnScanSuccessed mOnScanSuccessed;
	private final int mOriginalScreenOrientation;
	private final boolean mBeep;
	private final boolean mVibrate;

	final Vector<BarcodeFormat> mDecodeFormats;
	final String mCharacterSet;
	final boolean mUseOneShotPreviewCallback = Integer.parseInt(Build.VERSION.SDK) > 3; // 3 = Cupcake;

	final Camera mCamera;
	final CameraPreviewCallback mPreviewCallback;
	final CameraAutoFocusCallback mAutoFocusCallback;
	final Rect mFramingRect;
	final Rect mFramingRectInPreview;
	boolean mPreviewing = false;

	Point mScreenResolution;
	Point mCameraResolution;
	int mPreviewFormat;
	String mPreviewFormatString;

	public BarcodeCaptureDialog(Context context, OnScanSuccessed onScanSuccessed, boolean playBeep, boolean vibrate, boolean flash) {

		super(context, android.R.style.Theme_Panel);

		mContext = context;
		mDialog = this;

		DisplayMetrics displayMetrics = new DisplayMetrics();
		((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
		mOriginalScreenOrientation = (displayMetrics.widthPixels > displayMetrics.heightPixels) ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;

		((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

		mOnScanSuccessed = onScanSuccessed;
		mCharacterSet = null;
		mDecodeFormats = new Vector<BarcodeFormat>();
		mDecodeFormats.addAll(FORMATS_ONE_D);
		mDecodeFormats.addAll(FORMATS_QR_CODE);
		mDecodeFormats.addAll(FORMATS_DATA_MATRIX);

		mVibrate = vibrate;
		if (playBeep) {
			AudioManager audioService = (AudioManager) ((Activity) mContext).getSystemService(Context.AUDIO_SERVICE);
			if (audioService.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
				mBeep = false;
			} else {
				try {
					setVolumeControlStream(AudioManager.STREAM_MUSIC);
					mediaPlayer = new MediaPlayer();
					mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
					mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
						@Override
						public void onCompletion(MediaPlayer mediaPlayer) {
							mediaPlayer.seekTo(0);
						}
					});
					AssetFileDescriptor file = mContext.getResources().getAssets().openFd("beep.ogg");
					mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength());
					file.close();
					mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
					mediaPlayer.prepare();
				} catch (IOException e) {
					mediaPlayer = null;
				}
				mBeep = true;
			}
		} else {
			mBeep = false;
		}
		if (flash) {
			try {
				setFlashlight(true);
			} catch (Exception e) {
				error(e.getLocalizedMessage());
			}
		}

		this.setOnKeyListener(new DialogInterface.OnKeyListener() {
			@Override
			public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
				if (keyCode == KeyEvent.KEYCODE_BACK) {
					dismissDialog(null, null, null, null, null, null);
					return true;
				} else if (keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_FOCUS || keyCode == KeyEvent.KEYCODE_CAMERA) {
					return true;
				} else {
					return false;
				}
			}
		});

		try {
			Window window = getWindow();
			window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
		} catch (Exception e) {
			error(e.getLocalizedMessage());
		}

		FrameLayout frame = new FrameLayout(mContext);
		SurfaceView surfaceView = new SurfaceView(mContext);
		mViewfinderView = new ViewfinderView(this);
		try {
			mViewfinderView.setBackgroundColor(Color.TRANSPARENT);
			frame.addView(surfaceView);
			frame.addView(mViewfinderView);
			setContentView(frame);
		} catch (Exception e) {
			error(e.getLocalizedMessage());
		}

		mCamera = Camera.open();
		try {
			initCamera();
		} catch (Exception e) {
			error(e.getLocalizedMessage());
		}
		mPreviewCallback = new CameraPreviewCallback(this);
		mAutoFocusCallback = new CameraAutoFocusCallback();

		int width = mCameraResolution.x * 3 / 4;
		if (width < MIN_FRAME_WIDTH) {
			width = MIN_FRAME_WIDTH;
		} else if (width > MAX_FRAME_WIDTH) {
			width = MAX_FRAME_WIDTH;
		}
		int height = mCameraResolution.y * 3 / 4;
		if (height < MIN_FRAME_HEIGHT) {
			height = MIN_FRAME_HEIGHT;
		} else if (height > MAX_FRAME_HEIGHT) {
			height = MAX_FRAME_HEIGHT;
		}
		int leftOffset = (mScreenResolution.x - width) / 2;
		int topOffset = (mScreenResolution.y - height) / 2;
		mFramingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);

		Rect rect = new Rect(mFramingRect);
		rect.left = rect.left * mCameraResolution.x / mScreenResolution.x;
		rect.right = rect.right * mCameraResolution.x / mScreenResolution.x;
		rect.top = rect.top * mCameraResolution.y / mScreenResolution.y;
		rect.bottom = rect.bottom * mCameraResolution.y / mScreenResolution.y;
		mFramingRectInPreview = rect;

		SurfaceHolder surfaceHolder = surfaceView.getHolder();
		surfaceHolder.addCallback(new SurfaceHolder.Callback() {
			@Override
			public void surfaceCreated(SurfaceHolder holder) {
				try {
					mCamera.setPreviewDisplay(holder);
					mCaptureHandler = new CaptureHandler(mDialog);
				} catch (Exception e) {
					dismissDialog(null, null, null, null, null, null);
					error(e.getLocalizedMessage());
				}
			}
			@Override
			public void surfaceDestroyed(SurfaceHolder holder) {
			}
			@Override
			public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
			}
		});
		surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

	}

	private void error(final String message) {
		((Activity) mContext).runOnUiThread(new Runnable() {
			@Override
			public void run() {
				Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
			}
		});
	}

	public void startDialog() {
		show();
		Window window = getWindow();
		WindowManager.LayoutParams attr = window.getAttributes();
		attr.x = 0;
		attr.y = 0;
		attr.width = ViewGroup.LayoutParams.FILL_PARENT;
		attr.height = ViewGroup.LayoutParams.FILL_PARENT;
		window.setAttributes(attr);
	}
	void dismissDialog(Bitmap bitmap, String text, String format, String type, String metaData, String content) {
		if (mCaptureHandler != null) {
			mCaptureHandler.quitSynchronously();
			mCaptureHandler = null;
		}
		setFlashlight(false);
		mCamera.release();
		dismiss();
		((Activity) mContext).setRequestedOrientation(mOriginalScreenOrientation);
		mOnScanSuccessed.run(mContext, bitmap, text, format, type, metaData, content);
	}
	private void initCamera() {

		Camera.Parameters parameters = mCamera.getParameters();

		mPreviewFormat = parameters.getPreviewFormat();
		mPreviewFormatString = parameters.get("preview-format");
		WindowManager manager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
		Display display = manager.getDefaultDisplay();
		mScreenResolution = new Point(display.getWidth(), display.getHeight());

		mCameraResolution = null;
		String previewSizeValueString = parameters.get("preview-size-values");
		// saw this on Xperia
		if (previewSizeValueString == null) {
			previewSizeValueString = parameters.get("preview-size-value");
		}
		if (previewSizeValueString != null) {
			int bestX = 0, bestY = 0, diff = Integer.MAX_VALUE;
			for (String previewSize : COMMA_PATTERN.split(previewSizeValueString)) {
				previewSize = previewSize.trim();
				int dimPosition = previewSize.indexOf('x');
				if (dimPosition < 0) {
					continue;
				}
				int newX, newY;
				try {
					newX = Integer.parseInt(previewSize.substring(0, dimPosition));
					newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
				} catch (NumberFormatException nfe) {
					continue;
				}
				int newDiff = Math.abs(newX - mScreenResolution.x) + Math.abs(newY - mScreenResolution.y);
				if (newDiff == 0) {
					bestX = newX;
					bestY = newY;
					break;
				} else if (newDiff < diff) {
					bestX = newX;
					bestY = newY;
					diff = newDiff;
				}
			}
			mCameraResolution = (bestX > 0 && bestY > 0) ? new Point(bestX, bestY) : null;
		}
		if (mCameraResolution == null) {
			// Ensure that the camera resolution is a multiple of 8, as the screen may not be.
			mCameraResolution = new Point((mScreenResolution.x >> 3) << 3, (mScreenResolution.y >> 3) << 3);
		}
		parameters.setPreviewSize(mCameraResolution.x, mCameraResolution.y);

		// This is a hack to turn the flash off on the Samsung Galaxy. And this is a hack-hack to work around a different value on the Behold II. Restrict Behold II check to Cupcake, per Samsung's advice
		if (Build.MODEL.contains("Behold II") && Integer.parseInt(Build.VERSION.SDK) == 3) { // 3 = Cupcake
			parameters.set("flash-value", 1);
		} else {
			parameters.set("flash-value", 2);
		}
		// This is the standard setting to turn the flash off that all devices should honor.
		parameters.set("flash-mode", "off");

		String zoomSupportedString = parameters.get("zoom-supported");
		if (zoomSupportedString != null && !Boolean.parseBoolean(zoomSupportedString)) { return; }
		int tenDesiredZoom = TEN_DESIRED_ZOOM;
		String maxZoomString = parameters.get("max-zoom");
		if (maxZoomString != null) {
			try {
				int tenMaxZoom = (int) (10.0 * Double.parseDouble(maxZoomString));
				if (tenDesiredZoom > tenMaxZoom) {
					tenDesiredZoom = tenMaxZoom;
				}
			} catch (NumberFormatException nfe) {
			}
		}
		String takingPictureZoomMaxString = parameters.get("taking-picture-zoom-max");
		if (takingPictureZoomMaxString != null) {
			try {
				int tenMaxZoom = Integer.parseInt(takingPictureZoomMaxString);
				if (tenDesiredZoom > tenMaxZoom) {
					tenDesiredZoom = tenMaxZoom;
				}
			} catch (NumberFormatException nfe) {
			}
		}
		String motZoomValuesString = parameters.get("mot-zoom-values");
		if (motZoomValuesString != null) {
			int tenBestValue = 0;
			for (String stringValue : COMMA_PATTERN.split(motZoomValuesString)) {
				stringValue = stringValue.trim();
				double value;
				try {
					value = Double.parseDouble(stringValue);
				} catch (NumberFormatException nfe) {
					break;
				}
				int tenValue = (int) (10.0 * value);
				if (Math.abs(tenDesiredZoom - value) < Math.abs(tenDesiredZoom - tenBestValue)) {
					tenBestValue = tenValue;
				}
			}
			tenDesiredZoom = tenBestValue;
		}
		String motZoomStepString = parameters.get("mot-zoom-step");
		if (motZoomStepString != null) {
			try {
				double motZoomStep = Double.parseDouble(motZoomStepString.trim());
				int tenZoomStep = (int) (10.0 * motZoomStep);
				if (tenZoomStep > 1) {
					tenDesiredZoom -= tenDesiredZoom % tenZoomStep;
				}
			} catch (NumberFormatException nfe) {
				// continue
			}
		}
		// Set zoom. This helps encourage the user to pull back. Some devices like the Behold have a zoom parameter
		if (maxZoomString != null || motZoomValuesString != null) {
			parameters.set("zoom", String.valueOf(tenDesiredZoom / 10.0));
		}
		// Most devices, like the Hero, appear to expose this zoom parameter. It takes on values like "27" which appears to mean 2.7x zoom
		if (takingPictureZoomMaxString != null) {
			parameters.set("taking-picture-zoom", tenDesiredZoom);
		}
		mCamera.setParameters(parameters);
	}
	private void setFlashlight(boolean enableFlashlight) {
		try {

			Class<?> serviceManagerClass = Class.forName("android.os.ServiceManager");
			if (serviceManagerClass == null) { return; }
			Method getServiceMethod = serviceManagerClass.getMethod("getService", String.class);
			if (getServiceMethod == null) { return; }

			Class<?> iHardwareServiceStubClass = Class.forName("android.os.IHardwareService$Stub");
			if (iHardwareServiceStubClass == null) { return; }
			Method asInterfaceMethod = iHardwareServiceStubClass.getMethod("asInterface", IBinder.class);
			if (asInterfaceMethod == null) { return; }

			Object iHardwareService = asInterfaceMethod.invoke(null, getServiceMethod.invoke(null, "hardware"));
			if (iHardwareService == null) { return; }
			Method setFlashEnabledMethod = iHardwareService.getClass().getMethod("setFlashlightEnabled", boolean.class);
			if (setFlashEnabledMethod == null) { return; }

			setFlashEnabledMethod.invoke(iHardwareService, enableFlashlight);

		} catch (Exception e) {
			return;
		}
	}
	public void decode(Result rawResult, Bitmap barcode) {
		if (mBeep && mediaPlayer != null) {
			mediaPlayer.start();
		}
		if (mVibrate) {
			Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
			vibrator.vibrate(VIBRATE_DURATION);
		}

		ResultPoint[] points = rawResult.getResultPoints();
		if (points != null && points.length > 0) {
			Canvas canvas = new Canvas(barcode);
			Paint paint = new Paint();
			paint.setColor(android.R.color.white);
			paint.setStrokeWidth(3.0f);
			paint.setStyle(Paint.Style.STROKE);
			Rect border = new Rect(2, 2, barcode.getWidth() - 2, barcode.getHeight() - 2);
			canvas.drawRect(border, paint);
			paint.setColor(android.R.color.darker_gray);
			if (points.length == 2) {
				paint.setStrokeWidth(4.0f);
				canvas.drawLine(points[0].getX(), points[0].getY(), points[1].getX(), points[1].getY(), paint);
			} else if (points.length == 4 && (rawResult.getBarcodeFormat().equals(BarcodeFormat.UPC_A)) || (rawResult.getBarcodeFormat().equals(BarcodeFormat.EAN_13))) {
				// Hacky special case -- draw two lines, for the barcode and metadata
				canvas.drawLine(points[0].getX(), points[0].getY(), points[1].getX(), points[1].getY(), paint);
				canvas.drawLine(points[2].getX(), points[2].getY(), points[3].getX(), points[3].getY(), paint);
			} else {
				paint.setStrokeWidth(10.0f);
				for (ResultPoint point : points) {
					canvas.drawPoint(point.getX(), point.getY(), paint);
				}
			}
		}

		boolean bulk = false;
		if (bulk) {
			error("Bulk mode: barcode scanned and saved.");
			if (mCaptureHandler != null) {
				// Wait a moment or else it will scan the same barcode continuously about 3 times
				mCaptureHandler.sendEmptyMessageDelayed(DecodeHandler.MESSAGE_RESTART_PREVIEW, BULK_MODE_SCAN_DELAY_MS); //restart preview
			}
			mViewfinderView.setVisibility(View.VISIBLE);
		} else {
			mViewfinderView.setVisibility(View.GONE);
			String metaData = "null";
			@SuppressWarnings("unchecked")
			Map<ResultMetadataType, Object> metadata = rawResult.getResultMetadata();
			if (metadata != null) {
				StringBuilder metadataText = new StringBuilder(20);
				for (Map.Entry<ResultMetadataType, Object> entry : metadata.entrySet()) {
					if (DISPLAYABLE_METADATA_TYPES.contains(entry.getKey())) {
						metadataText.append(entry.getValue()).append('\n');
					}
				}
				if (metadataText.length() > 0) {
					metaData = metadataText.toString();
				}
			}
			ParsedResult result = ResultParser.parseResult(rawResult);
			dismissDialog(barcode, rawResult.getBarcodeFormat().toString(), rawResult.getText(), result.getType().toString(), metaData, result.getDisplayResult().replace("\r", "").toString());
		}

	}

	public static abstract class OnScanSuccessed {
		public abstract void run(Context context, Bitmap bitmap, String text, String format, String type, String metaData, String content);
	}

}
